#pragma once

#include <functional>
#include <memory>
#include <unordered_set>

#include "util/util.hpp"

namespace terms {
	/**
	 * \brief Term instance/singleton caching.
	 *
	 * The TermCache is a helper class for \ref TermFactory.
	 *
	 * It stores a pointer to all terms submitted to it.
	 * When trying to submit a new instance of a term that is
	 * already known to the cache (based on object equality, not on
	 * pointer equality), the cached instance is returned.o
	 *
	 * If an uncached Term is submitted, the cache stores a reference
	 * to the new term and also takes ownership of the Term before
	 * returning the freshly stored value.
	 *
	 * All cached terms are released when the cache is destroyed.
	 *
	 * TermCaches can nest, i.e. a TermCache can refer to a base cache.
	 * Term lookup cascades recursively to base caches, i.e. a term
	 * that is not available in the current cache is looked up in 
	 * the base caches and possible returned from there. A new term
	 * is only stored when a submitted term is not available in the
	 * current cache or any of its base caches.
	 *
	 * \tparam BaseTerm The base term of the \ref TermSystem. This is
	 * the superclass of all terms this term cache can store.
	 **/
	template <typename BaseTerm>
	class TermCache {
	public:
		typedef std::unordered_set<BaseTerm *, util::PtrHash<BaseTerm, std::hash<Term>>, util::PtrDeepEqual<BaseTerm>> TermSet;

	private:
		std::shared_ptr<const TermCache<BaseTerm> > _base_cache;

	private:
		TermSet _cache;

	public:
		TermCache()
			: _base_cache(nullptr), _cache()
		{
		}

	public:
		/**
		 * \brief Create a new term cache with an associated base cache.
		 *
		 * The term cache will keep a reference to the base cache
		 * until destruction.
		 **/
		TermCache(std::shared_ptr<TermCache<BaseTerm> > base_cache)
			: _base_cache(base_cache), _cache()
		{
		}

	public:
		/**
		 * Destroy the term cache and free all terms
		 * managed by this term cache
		 */
		~TermCache()
		{
			typename TermSet::iterator iter = _cache.begin();
			while (iter != _cache.end()) {
				delete *iter;
				iter = _cache.erase(iter);
			}
		}


	public:
		/**
		 * \brief Try to find a particular term in the cache.
		 *
		 * \returns an iterator pointing to the term in question.
		 * The iterator may be from the current cache or from
		 * any of the base caches.
		 *
		 * Returns an \ref util::EndAwareIterator that is past-the-end
		 * if the supplied term could not be found in the current cache
		 * or any of its base caches.
		 *
		 * \tparam Term the type of the term to look for.
		 **/
		template <typename Term>
		util::EndAwareIterator<typename TermSet::const_iterator>
		find(Term *term) const
		{
			typename TermSet::const_iterator pos = _cache.find(term);
			if ((pos == _cache.end()) && _base_cache) {
				return _base_cache->find(term);
			} else {
				return util::EndAwareIterator<typename TermSet::const_iterator>(_cache, pos);
			}
		}
		
	public:
		/**
		 * \brief Retrieve or store a term inside the cache.
		 *
		 * Tries to find a term equal (using object equality, not
		 * pointer equality) to the parameter term.
		 *
		 * If a term is found, the cached term is returned
		 * and term_ptr is not modified.
		 *
		 * If no term is found, the cache takes ownership of
		 * the passed in term (using \ref std::unique_ptr::release())
		 * and returns the passed in value.
		 *
		 * \return A cached term matching the parameter term.
		 * \tparam Term The type of the term to retrieve/store.
		 **/
		template <typename Term>
		const Term *get(std::unique_ptr<Term> &term_ptr)
		{
			auto cache_result = find(term_ptr.get());
			if (! cache_result) {
				Term *term = term_ptr.get();
				_cache.insert(term);
				term_ptr.release();
				cache_result = find(term);
			}
			auto result = cache_result.get();

			return dynamic_cast<Term *>(*result);
		}
	};
}

